﻿using System;
using System.Reflection;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;

namespace LINQToolKit.Collections
{
    public static class EnumerableHelper
    {
        public static MethodInfo GetMethod(
            string methodName, IList<Type>  argumentTypes, Type sourceType, Type resultType)
        {
            MethodInfo method = null;

            var collection =
                from m in typeof(Enumerable).GetMethods(BindingFlags.Static | BindingFlags.Public)
                let parameters = m.GetParameters()
                where m.Name == methodName && parameters.Length == argumentTypes.Count
                select new { Method = m, Parameters = parameters };

            foreach (var item in collection)
            {
                var types1 =
                    from p in item.Parameters
                    select p.ParameterType;

                if (AreTypesMatch(types1.ToList(), argumentTypes))
                {
                    method = item.Method;
                    break;
                }
            }

            if (method != null && method.IsGenericMethod)
            {
                int genericArgCount = method.GetGenericArguments().Length;

                if (genericArgCount == 1)
                {
                    method = method.MakeGenericMethod(sourceType);
                }
                else
                {
                    switch (methodName) 
                    {
                        case "Select":
                            {
                                method = method.MakeGenericMethod(sourceType, resultType);
                                break;
                            }
                        case "OrderBy":
                        case "OrderByDescending":
                            {
                                Type keyType = argumentTypes[1].GetGenericArguments()[1];
                                method = method.MakeGenericMethod(sourceType, keyType);
                                break;
                            }
                        case "SelectMany":
                            {
                                if (genericArgCount == 2)
                                {    
                                    method = method.MakeGenericMethod(sourceType, resultType);
                                }
                                else
                                {
                                    Type collectionType = 
                                        argumentTypes[1].GetGenericArguments().Last();
                                    method = method.MakeGenericMethod(
                                        sourceType, collectionType, resultType);
                                }

                                break;
                            }
                        default:
                            break;
                    }
                }
            }

            return method;
        }

        private static bool AreTypesMatch(IList<Type> list, IList<Type> list2)
        {
            for (int i = 0; i < list2.Count; i++)
            {
                if (list[i].IsGenericType)
                {
                    Type genericType = list[i].GetGenericTypeDefinition();
                    int args = genericType.GetGenericArguments().Length;
                    genericType = genericType.MakeGenericType(
                        list2[i].GetGenericArguments().Take(args).ToArray());

                    if (genericType == null || !genericType.IsAssignableFrom(list2[i]))
                    {
                        return false;
                    }
                }
                else
                {
                    if (list[i] != list2[i])
                    {
                        return false;
                    }
                }
            }

            return true;
        }

        public static IList<object> GetArgumentValues(IList<Expression> argumentExprs)
        {
            IList<object> arguments = new List<object>();

            foreach (var item in argumentExprs)
            {
                if (item is ConstantExpression)
                {
                    ConstantExpression expr = (ConstantExpression)item;
                    arguments.Add(expr.Value);
                }
                else if (item is LambdaExpression)
                {
                    LambdaExpression expr = (LambdaExpression)item;
                    arguments.Add(expr.Compile());
                }
                else if (item is UnaryExpression)
                {
                    UnaryExpression uexpr = (UnaryExpression)item;

                    if (uexpr.Operand is LambdaExpression)
                    {
                        LambdaExpression expr = (LambdaExpression)uexpr.Operand;
                        arguments.Add(expr.Compile());
                    }
                    else
                    {
                        throw new ArgumentException(item.ToString());
                    }
                }
                else
                {
                    throw new ArgumentException(item.ToString());
                }
            }

            return arguments;
        }
    }
}
